package com.yhn.bubblevideo.video.service.impl;

import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.*;
import com.yhn.bubblevideo.common.constant.FileConstant;
import com.yhn.bubblevideo.common.constant.RedisConstant;
import com.yhn.bubblevideo.common.exception.BusinessException;
import com.yhn.bubblevideo.model.dto.req.video.UploadingFileDTO;
import com.yhn.bubblevideo.video.config.OssProperties;
import com.yhn.bubblevideo.video.service.VideoUploadService;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @author yhn
 * @version 1.0
 * @description
 * @date 2024/12/20 15:48
 */
@Service
@Slf4j
public class VideoUploadServiceImpl implements VideoUploadService {

    @Resource
    private OssProperties ossProperties;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public String uploadVideo(MultipartFile file, Integer chunkIndex, String uploadId) {
        String filename = file.getOriginalFilename();
        if (StringUtils.isBlank(file.getOriginalFilename())) {
            throw new BusinessException("上传的视频视频有误");
        }
        //先从Redis取出对象
        String jsonStr = stringRedisTemplate.opsForValue().get(RedisConstant.VIDEO_UPLOAD_TEMP_KEY + uploadId);
        if (jsonStr == null) {
            throw new BusinessException("文件不存在请重新上传");
        }
        UploadingFileDTO uploadingFileDTO = JSONUtil.toBean(jsonStr, UploadingFileDTO.class);
        String uploadFileId = uploadingFileDTO.getUploadId();
        if (uploadFileId == null) {
            throw new BusinessException("文件不存在请重新上传");
        }

        if (file.getSize() > FileConstant.CHUNK_SIZE) {
            throw new BusinessException("分片大小不能超过5M");
        }
        //判断分片,第一个判断不能重复串
        if (chunkIndex < 0 || !chunkIndex.equals(uploadingFileDTO.getChunkIndex() + 1) || chunkIndex > uploadingFileDTO.getChunkSize()) {
            throw new BusinessException("分片有误，请重新上传");
        }
        //实时记录当前传了多少分片和大小
        uploadingFileDTO.setChunkIndex(chunkIndex);
        uploadingFileDTO.setSize(uploadingFileDTO.getSize() + file.getSize());

        filename = FileConstant.OSS_VIDEO_PATH + uploadingFileDTO.getFolder() + chunkIndex;
        String bucketName = ossProperties.getBucketName();
        OSS ossClient = createOssClient();
        //判断是否为最后一片
        long curPartSize = (chunkIndex.equals(uploadingFileDTO.getChunkSize()))
                ? file.getSize() : FileConstant.CHUNK_SIZE;
        // partETags是PartETag的集合。PartETag由分片的ETag和分片号组成。
        List<PartETag> partETags;
        String objectName = uploadingFileDTO.getFileName();
        // 将 MultipartFile 转换为 byte 数组
        byte[] bytes;
        try {
            bytes = file.getBytes();
        } catch (IOException e) {
            throw new BusinessException("读取文件内容失败");
        }

        // 使用 byte 数组创建新的 InputStream
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        try {
            UploadPartRequest uploadPartRequest = new UploadPartRequest();
            uploadPartRequest.setBucketName(bucketName);
            uploadPartRequest.setKey(objectName);
            uploadPartRequest.setUploadId(uploadId);
            uploadPartRequest.setPartSize(curPartSize);
            uploadPartRequest.setInputStream(byteArrayInputStream);
            uploadPartRequest.setPartNumber(chunkIndex);
            UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
            PartETag partETag = uploadPartResult.getPartETag();
            //redis更新数值
            stringRedisTemplate.opsForValue()
                    .set(RedisConstant.VIDEO_UPLOAD_TEMP_KEY + uploadId, JSONUtil.toJsonStr(uploadingFileDTO), RedisConstant.VIDEO_UPLOAD_TEMP_TIME, TimeUnit.DAYS);

            // 保存PartETag到Redis或者其他地方，以便最后一步合并使用
            partETags = getPartETagsFromRedis(uploadId);
            partETags.add(partETag);
            savePartETagsToRedis(uploadId, partETags);
        } catch (Exception e) {
            log.error("上传到oss错误" + e.getMessage());
            throw new BusinessException("上传到oss错误");
        }
        // 关闭OSSClient。
        ossClient.shutdown();
        return "https://" + bucketName + "." + ossProperties.getEndpoint() + "/" + filename;
    }

    @Override
    @SneakyThrows
    public String uploadImage(MultipartFile file) {
        String filename = file.getOriginalFilename();
        if (StringUtils.isBlank(filename)) {
            throw new BusinessException("上传的图片文件有误");
        }
        String contentType = file.getContentType();
        // 验证文件名和MIME类型
        if (StringUtils.isBlank(contentType)) {
            throw new BusinessException("你必须上传图片文件");
        }
        boolean isImage = Arrays.stream(FileConstant.IMAGE_MIME_TYPES).anyMatch(contentType::startsWith);
        if (!isImage) {
            throw new BusinessException("你必须上传图片文件");
        }
        filename = FileConstant.OSS_IMAGE_PATH + dealWithoutFilename(filename);
        String endpoint = ossProperties.getEndpoint();
        String bucketName = ossProperties.getBucketName();
        OSS ossClient = createOssClient();
        try {
            ossClient.putObject(bucketName, filename, file.getInputStream());
        } catch (IOException e) {
            log.error("上传到oss错误" + e.getMessage());
            throw new BusinessException("上传到oss错误");
        }
        // 关闭OSSClient。
        ossClient.shutdown();

        return "https://" + bucketName + "." + endpoint + "/" + filename;
    }

    /**
     * 遇上传，返回视频唯一ID
     *
     * @param fileName  文件名称
     * @param chunkSize 分片个数
     * @return 生成的视频唯一ID
     */
    @Override
    public String preUpload(String fileName, Integer chunkSize) {
        UploadingFileDTO uploadingFileDTO = new UploadingFileDTO();

        OSS ossClient = createOssClient();
        String newFileName = FileConstant.OSS_VIDEO_PATH
                + UUID.randomUUID() + fileName.substring(fileName.lastIndexOf("."));
        //初始化分片请求
        InitiateMultipartUploadRequest initRequest = new InitiateMultipartUploadRequest(ossProperties.getBucketName(), newFileName);
        InitiateMultipartUploadResult initResponse = ossClient.initiateMultipartUpload(initRequest);
        String uploadId = initResponse.getUploadId();
        // 结合更多信息生成更复杂的唯一ID
        uploadingFileDTO.setUploadId(uploadId)
                .setChunkIndex(0)
                .setChunkSize(chunkSize)
                .setFileName(newFileName)
                .setSize(0L)
                .setFolder(uploadId + FileConstant.OSS_VIDEO_TEMP_PATH);
        //保存ID到Redis，时间为1天
        stringRedisTemplate.opsForValue()
                .set(RedisConstant.VIDEO_UPLOAD_TEMP_KEY + uploadId, JSONUtil.toJsonStr(uploadingFileDTO), RedisConstant.VIDEO_UPLOAD_TEMP_TIME, TimeUnit.DAYS);
        return uploadId;
    }

    @Override
    public Boolean deleteUploadVideo(String uploadId) {
        //先从Redis取出对象
        String jsonStr = stringRedisTemplate.opsForValue().get(RedisConstant.VIDEO_UPLOAD_TEMP_KEY + uploadId);
        if (jsonStr == null) {
            throw new BusinessException("文件不存在请重新上传");
        }
        UploadingFileDTO uploadingFileDTO = JSONUtil.toBean(jsonStr, UploadingFileDTO.class);
        //删除redis的信息
        stringRedisTemplate.delete(RedisConstant.VIDEO_UPLOAD_TEMP_KEY + uploadId);
        OSS ossClient = createOssClient();
        // 构造批量删除请求
        DeleteObjectsRequest deleteObjectsRequest = new DeleteObjectsRequest(ossProperties.getBucketName());

        // 根据上传ID和分片数量收集所有相关文件名
        List<String> keysToDelete = new ArrayList<>();
        for (int i = 1; i <= uploadingFileDTO.getChunkSize(); i++) {
            String filename = FileConstant.OSS_VIDEO_PATH + uploadingFileDTO.getFolder() + i;
            keysToDelete.add(filename);
        }

        // 添加对象键到批量删除请求
        deleteObjectsRequest.setKeys(keysToDelete);

        // 执行批量删除操作
        DeleteObjectsResult deleteObjectsResult = ossClient.deleteObjects(deleteObjectsRequest);
        ossClient.shutdown();
        return true;
    }


    @Override
    public String completeMultipartUpload(String uploadId) {
        // 从Redis取出对象
        String jsonStr = stringRedisTemplate.opsForValue().get(RedisConstant.VIDEO_UPLOAD_TEMP_KEY + uploadId);
        if (jsonStr == null) {
            throw new BusinessException("文件不存在请重新上传");
        }
        UploadingFileDTO uploadingFileDTO = JSONUtil.toBean(jsonStr, UploadingFileDTO.class);

        String objectName = uploadingFileDTO.getFileName(); // 假设filename就是objectName
        List<PartETag> partETags = getPartETagsFromRedis(uploadId); // 获取所有的PartETag

        OSS ossClient = createOssClient();
        try {
            ListPartsRequest listPartsRequest = new ListPartsRequest(ossProperties.getBucketName(), objectName, uploadId);
            PartListing partListing = ossClient.listParts(listPartsRequest);
            log.info("Listed {} parts for uploadId: {}", partListing.getParts().size(), uploadId);

            // Debug: Log each part ETag and number for verification
            for (PartETag partETag : partETags) {
                log.info("Part Number: {}, ETag: {}", partETag.getPartNumber(), partETag.getETag());
            }

            CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest(
                    ossProperties.getBucketName(), objectName, uploadId, partETags);
            CompleteMultipartUploadResult result = ossClient.completeMultipartUpload(completeRequest);

            // 清理临时资源
            stringRedisTemplate.delete(RedisConstant.VIDEO_UPLOAD_PART_ETAGS_KEY + uploadId);
            deleteUploadVideo(uploadId);

            return result.getLocation();
        } catch (Exception e) {
            log.error("合并分片失败: " + e.getMessage(), e);
            throw new BusinessException("合并分片失败");
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }

    /**
     * partETags：一个包含每个已上传分片的ETag和分片编号的列表。OSS会根据这个列表来验证每个分片，并按照分片编号排序后合并成完整的文件。
     */
    private List<PartETag> getPartETagsFromRedis(String uploadId) {
        // 从Redis中获取序列化的PartETags JSON字符串
        String partETagsJson = stringRedisTemplate.opsForValue().get(RedisConstant.VIDEO_UPLOAD_PART_ETAGS_KEY + uploadId);

        // 如果不存在，则返回空列表
        if (StringUtils.isBlank(partETagsJson)) {
            return new ArrayList<>();
        }

        // 将JSON字符串反序列化为PartETag对象列表
        return JSON.parseArray(partETagsJson, PartETag.class);
    }

    // 辅助方法：保存PartETags列表到Redis
    private void savePartETagsToRedis(String uploadId, List<PartETag> partETags) {
        // 将PartETags列表序列化为JSON字符串
        String partETagsJson = JSON.toJSONString(partETags);

        // 存储到Redis中，设置过期时间以确保临时数据不会无限期占用空间
        stringRedisTemplate.opsForValue()
                .set(RedisConstant.VIDEO_UPLOAD_PART_ETAGS_KEY + uploadId, partETagsJson, RedisConstant.VIDEO_UPLOAD_TEMP_TIME, TimeUnit.DAYS);
    }

    /**
     * 处理文件名
     *
     * @return 处理后的文件名称
     */
    private String dealWithoutFilename(String filename) {
        String suffixName = filename.substring(filename.lastIndexOf("."));
        String uuid = UUID.randomUUID().toString();
        return uuid + suffixName;
    }

    /**
     * 创建oss客户端
     */
    private OSS createOssClient() {
        return new OSSClientBuilder().build(
                ossProperties.getEndpoint(),
                ossProperties.getKeyId(),
                ossProperties.getKeySecret()
        );
    }
}
